home *** CD-ROM | disk | FTP | other *** search
/ PC World Komputer 2010 April / PCWorld0410.iso / hity wydania / Ubuntu 9.10 PL / karmelkowy-koliberek-desktop-9.10-i386-PL.iso / casper / filesystem.squashfs / usr / bin / ubuntuone-client-applet < prev    next >
Text File  |  2009-10-29  |  42KB  |  1,051 lines

  1. #!/usr/bin/python
  2.  
  3. # ubuntuone-client-applet - Tray icon applet for managing Ubuntu One
  4. #
  5. # Author: Rodney Dawes <rodney.dawes@canonical.com>
  6. #
  7. # Copyright 2009 Canonical Ltd.
  8. #
  9. # This program is free software: you can redistribute it and/or modify it
  10. # under the terms of the GNU General Public License version 3, as published
  11. # by the Free Software Foundation.
  12. #
  13. # This program is distributed in the hope that it will be useful, but
  14. # WITHOUT ANY WARRANTY; without even the implied warranties of
  15. # MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
  16. # PURPOSE.  See the GNU General Public License for more details.
  17. #
  18. # You should have received a copy of the GNU General Public License along
  19. # with this program.  If not, see <http://www.gnu.org/licenses/>.
  20.  
  21. from __future__ import with_statement
  22.  
  23. import pygtk
  24. pygtk.require('2.0')
  25. import gobject
  26. import gtk
  27. import pango
  28. import os
  29. import subprocess
  30. import sys
  31. import gettext
  32. from ubuntuone import clientdefs
  33.  
  34. import dbus.service
  35.  
  36. # pylint: disable-msg=F0401
  37. import pynotify
  38.  
  39. from ConfigParser import ConfigParser
  40. from dbus.exceptions import DBusException
  41. from dbus.mainloop.glib import DBusGMainLoop
  42. from ubuntuone.oauthdesktop.main import Login
  43. from xdg.BaseDirectory import xdg_config_home
  44. from threading import Lock
  45. from urllib import quote
  46.  
  47. from ubuntuone.oauthdesktop.logger import setupLogging
  48. logger = setupLogging("UbuntuOne.Client.Applet")
  49.  
  50. DBusGMainLoop(set_as_default=True)
  51.  
  52. _ = gettext.gettext
  53. P_ = gettext.ngettext
  54.  
  55. APPLET_BUS_NAME = "com.ubuntuone.ClientApplet"
  56. APPLET_CONFIG_NAME = APPLET_BUS_NAME + ".Config"
  57.  
  58. DBUS_IFACE_NAME = "com.ubuntuone.SyncDaemon"
  59. DBUS_IFACE_SYNC_NAME = "com.ubuntuone.SyncDaemon.SyncDaemon"
  60. DBUS_IFACE_STATUS_NAME = "com.ubuntuone.SyncDaemon.Status"
  61. DBUS_IFACE_CONFIG_NAME = "com.ubuntuone.SyncDaemon.Config"
  62.  
  63. DBUS_IFACE_AUTH_NAME = "com.ubuntuone.Authentication"
  64.  
  65. OAUTH_REALM = "https://ubuntuone.com"
  66. OAUTH_CONSUMER = "ubuntuone"
  67. BOOKMARK_NAME = "Ubuntu One"
  68.  
  69. NOTIFY_ICON_SIZE = 48
  70.  
  71. # Why thank you GTK+ for enforcing style-set and breaking API
  72. RCSTYLE = """
  73. style 'dialogs' {
  74.   GtkDialog::action-area-border = 12
  75.   GtkDialog::button-spacing = 6
  76.   GtkDialog::content-area-border = 0
  77. }
  78. widget_class '*Dialog*' style 'dialogs'
  79. """
  80.  
  81. CONF_FILE = os.path.join(xdg_config_home, "ubuntuone", "ubuntuone-client.conf")
  82.  
  83.  
  84. def dbus_async(*args):
  85.       """Simple handler to make dbus do stuff async."""
  86.       pass
  87.  
  88. class AppletMain(object):
  89.       """Main applet process class."""
  90.  
  91.       def __init__(self, *args, **kw):
  92.             """Initializes the child threads and dbus monitor."""
  93.             from twisted.internet import gtk2reactor
  94.             gtk2reactor.install()
  95.             login = Login(dbus.service.BusName(DBUS_IFACE_AUTH_NAME,
  96.                                                bus=dbus.SessionBus()))
  97.  
  98.             logger.info(_("Starting Ubuntu One client version %s") %
  99.                         clientdefs.VERSION)
  100.  
  101.             # Whether or not we are authorized
  102.             self.is_authorized = False
  103.  
  104.             # Load the config, with some defaults if it doesn't exist yet
  105.             if not os.path.isdir(os.path.dirname(CONF_FILE)):
  106.                   os.makedirs(os.path.dirname(CONF_FILE))
  107.  
  108.             self.config = ConfigParser()
  109.             self.config.read(CONF_FILE)
  110.  
  111.             if not self.config.has_section("ubuntuone"):
  112.                   self.config.add_section("ubuntuone")
  113.  
  114.             if not self.config.has_option("ubuntuone", "show_applet"):
  115.                   self.config.set("ubuntuone", "show_applet", "1")
  116.  
  117.             if not self.config.has_option("ubuntuone", "connect"):
  118.                   self.config.set("ubuntuone", "connect", "0")
  119.  
  120.             if not self.config.has_option("ubuntuone", "connected"):
  121.                   self.config.set("ubuntuone", "connected", "False")
  122.  
  123.             if not self.config.has_option("ubuntuone", "bookmarked"):
  124.                   self.config.set("ubuntuone", "bookmarked", "False")
  125.  
  126.             self.show_applet = self.config.getint("ubuntuone", "show_applet")
  127.             self.connect = self.config.getint("ubuntuone", "connect")
  128.             self.connected = self.config.getboolean("ubuntuone", "connected")
  129.  
  130.             if not os.path.exists(CONF_FILE):
  131.                   with open(CONF_FILE, "w+b") as f:
  132.                         self.config.write(f)
  133.  
  134.             # Handle some DBus signals
  135.             self.__bus = dbus.SessionBus()
  136.             self.__bus.add_signal_receiver(
  137.                   handler_function=self.__new_credentials,
  138.                   signal_name="NewCredentials",
  139.                   dbus_interface=DBUS_IFACE_AUTH_NAME)
  140.             self.__bus.add_signal_receiver(
  141.                   handler_function=self.__auth_denied,
  142.                   signal_name="AuthorizationDenied",
  143.                   dbus_interface=DBUS_IFACE_AUTH_NAME)
  144.             self.__bus.add_signal_receiver(
  145.                   handler_function=self.__no_credentials,
  146.                   signal_name="NoCredentials",
  147.                   dbus_interface=DBUS_IFACE_AUTH_NAME)
  148.             self.__bus.add_signal_receiver(
  149.                   handler_function=self.__got_oauth_error,
  150.                   signal_name="OAuthError",
  151.                   dbus_interface=DBUS_IFACE_AUTH_NAME)
  152.  
  153.             self.__icon = AppletIcon(main=self, config=self.config)
  154.  
  155.       def __new_credentials(self, realm=None, consumer_key=None, sender=None):
  156.             """Signal callback for when we get new credentials."""
  157.             self.is_authorized = True
  158.  
  159.             self.__start_storage_daemon()
  160.             self.add_to_autostart()
  161.             
  162.             self.set_up_desktopcouch_pairing(consumer_key)
  163.             
  164.             if self.connect == 2:
  165.                   return
  166.  
  167.             if self.connect == 1 and not self.connected:
  168.                   return
  169.  
  170.             try:
  171.                   client = self.__bus.get_object(DBUS_IFACE_NAME, "/",
  172.                                                  follow_name_owner_changes=True)
  173.                   iface = dbus.Interface(client, DBUS_IFACE_SYNC_NAME)
  174.                   iface.connect(reply_handler=dbus_async,
  175.                                 error_handler=self.sd_dbus_error)
  176.             except DBusException, e:
  177.                   self.sd_dbus_error(e)
  178.  
  179.       def __auth_denied(self):
  180.             """Signal callback for when auth was denied by user."""
  181.             self.is_authorized = False
  182.             self.remove_from_autostart()
  183.  
  184.             def quit_error(e):
  185.                   """Only log when quit fails."""
  186.                   logger.error(_("Quit Error: %s") % e.get_dbus_message())
  187.  
  188.             try:
  189.                   client = self.__bus.get_object(DBUS_IFACE_NAME, "/",
  190.                       follow_name_owner_changes=True)
  191.                   iface = dbus.Interface(client, DBUS_IFACE_SYNC_NAME)
  192.                   iface.quit(reply_handler=dbus_async,
  193.                              error_handler=quit_error)
  194.             except DBusException, e:
  195.                   quit_error(e)
  196.  
  197.             from twisted.internet import reactor
  198.             reactor.stop()
  199.  
  200.       def __no_credentials(self):
  201.             """Signal callback for when no credentials exist in the keyring."""
  202.             self.is_authorized = False
  203.  
  204.       def __got_oauth_error(self, message=None):
  205.             """Signal callback for when an OAuth error occured."""
  206.             def dialog_response(dialog, response):
  207.                   """Handle the dialog closing."""
  208.                   dialog.destroy()
  209.  
  210.             if message:
  211.                   logger.error(message)
  212.                   dialog = gtk.Dialog(title=_("Ubuntu One: Error"),
  213.                                       flags=gtk.DIALOG_NO_SEPARATOR,
  214.                                       buttons=(gtk.STOCK_CLOSE,
  215.                                                gtk.RESPONSE_CLOSE))
  216.                   dialog.set_default_response(gtk.RESPONSE_CLOSE)
  217.                   dialog.set_icon_name("ubuntuone-client")
  218.  
  219.                   area = dialog.get_content_area()
  220.  
  221.                   hbox = gtk.HBox(spacing=12)
  222.                   hbox.set_border_width(12)
  223.                   area.pack_start(hbox)
  224.                   hbox.show()
  225.  
  226.                   image = gtk.Image()
  227.                   image.set_from_icon_name("dialog-error", gtk.ICON_SIZE_DIALOG)
  228.                   image.set_alignment(0.5, 0.0)
  229.                   image.show()
  230.                   hbox.pack_start(image, False, False)
  231.  
  232.                   vbox = gtk.VBox(spacing=12)
  233.                   vbox.show()
  234.                   hbox.pack_start(vbox)
  235.  
  236.                   label = gtk.Label("<b>%s</b>" % _("Authorization Error"))
  237.                   label.set_use_markup(True)
  238.                   label.set_alignment(0.0, 0.5)
  239.                   label.show()
  240.                   vbox.pack_start(label, False, False)
  241.  
  242.                   label = gtk.Label(message)
  243.                   label.set_line_wrap(True)
  244.                   label.set_max_width_chars(64)
  245.                   label.set_ellipsize(pango.ELLIPSIZE_MIDDLE)
  246.                   label.set_alignment(0.0, 0.0)
  247.                   label.show()
  248.                   vbox.pack_start(label, True, True)
  249.  
  250.                   dialog.connect('close', dialog_response, gtk.RESPONSE_CLOSE)
  251.                   dialog.connect('response', dialog_response)
  252.  
  253.                   dialog.show()
  254.             else:
  255.                   logger.error(_("Got an OAuth error with no message."))
  256.  
  257.       def check_for_token(self, do_login=False):
  258.             """Method to check for an existing token."""
  259.             def local_dbus_error(e):
  260.                   """Can't talk to ourself?"""
  261.                   logger.error(_("Internal Error: %s") % e.get_dbus_message())
  262.  
  263.             try:
  264.                   client = self.__bus.get_object(DBUS_IFACE_AUTH_NAME, "/",
  265.                                                  follow_name_owner_changes=True)
  266.                   iface = dbus.Interface(client, DBUS_IFACE_AUTH_NAME)
  267.                   iface.maybe_login(OAUTH_REALM, OAUTH_CONSUMER,
  268.                                     do_login,
  269.                                     reply_handler=dbus_async,
  270.                                     error_handler=local_dbus_error)
  271.             except DBusException, e:
  272.                   local_dbus_error(e)
  273.                   return False
  274.  
  275.             return False
  276.  
  277.       def __start_storage_daemon_maybe(self):
  278.             """Start the storage daemon."""
  279.             try:
  280.                   client = self.__bus.get_object(DBUS_IFACE_NAME, "/",
  281.                                                  follow_name_owner_changes=True)
  282.                   iface = dbus.Interface(client, DBUS_IFACE_SYNC_NAME)
  283.                   iface.get_rootdir(reply_handler=dbus_async,
  284.                                     error_handler=self.sd_dbus_error)
  285.             except DBusException, e:
  286.                   self.sd_dbus_error(e)
  287.                   return False
  288.  
  289.             return False
  290.  
  291.       def __start_storage_daemon(self):
  292.             """Need to call dbus from a idle callback"""
  293.             gobject.idle_add(self.__start_storage_daemon_maybe)
  294.  
  295.       def add_to_autostart(self):
  296.             """Add ourself to the autostart config."""
  297.             autostart_entry = """[Desktop Entry]
  298. Name=Ubuntu One
  299. Exec=ubuntuone-client-applet
  300. Icon=ubuntuone-client
  301. Terminal=false
  302. Type=Application
  303. X-Ubuntu-Gettext-Domain=ubuntuone-client
  304. X-KDE-autostart-after=panel
  305. X-GNOME-Autostart-enabled=true
  306. """
  307.             if not os.path.exists(os.path.join(xdg_config_home, "autostart")):
  308.                   os.makedirs(os.path.join(xdg_config_home, "autostart"))
  309.  
  310.             file_path = os.path.join(xdg_config_home, "autostart",
  311.                                      "ubuntuone-client-applet.desktop")
  312.             if not os.path.exists(file_path):
  313.                   with open(file_path, "w+") as f:
  314.                         f.write(autostart_entry)
  315.  
  316.       def remove_from_autostart(self):
  317.             """Remove ourself from the autostart config."""
  318.             path = os.path.join(xdg_config_home, "autostart",
  319.                                 "ubuntuone-client-applet.desktop")
  320.             try:
  321.                   os.unlink(path)
  322.             except OSError:
  323.                   pass
  324.       
  325.       def set_up_desktopcouch_pairing(self, consumer_key):
  326.           """Add a pairing record between desktopcouch and Ubuntu One"""
  327.           try:
  328.               from desktopcouch.pair.couchdb_pairing.couchdb_io import \
  329.                    put_static_paired_service, PAIRED_SERVER_RECORD_TYPE
  330.               from desktopcouch.records.server import CouchDatabase
  331.           except ImportError:
  332.               # desktopcouch is not installed
  333.               logger.debug(_("Not adding desktopcouch pairing since"
  334.                   " desktopcouch is not installed"))
  335.               return
  336.           # Check whether there is already a record of the Ubuntu One service
  337.           db = CouchDatabase("management", create=True)
  338.           if not db.view_exists("ubuntu_one_pair_record","ubuntu_one_pair_record"):
  339.               map_js = """function(doc) {
  340.                   if (doc.service_name == "ubuntuone") {
  341.                       if (doc.application_annotations && 
  342.                           doc.application_annotations["Ubuntu One"] &&
  343.                           doc.application_annotations["Ubuntu One"]["private_application_annotations"] &&
  344.                           doc.application_annotations["Ubuntu One"]["private_application_annotations"]["deleted"]) {
  345.                           emit(doc._id, 1);
  346.                       } else {
  347.                           emit(doc._id, 0)
  348.                       }
  349.                   }
  350.               }"""
  351.               db.add_view("ubuntu_one_pair_record", map_js, None, 
  352.                   "ubuntu_one_pair_record")
  353.           results = db.execute_view("ubuntu_one_pair_record", 
  354.               "ubuntu_one_pair_record")
  355.           found = False
  356.           deleted = False
  357.           # Results should contain either one row or no rows
  358.           # If there is one row, its value will be 0, meaning that there is
  359.           #   already an Ubuntu One pairing record, or 1, meaning that there
  360.           #   was an Ubuntu One pairing record but it has since been unpaired
  361.           # Only create a new record if there is not one already. Specifically,
  362.           #   do not add the record if there is a deleted one, as this means
  363.           #   that the user explicitly unpaired it!
  364.           for row in results:
  365.               found = True
  366.               if row.value == 1:
  367.                   deleted = True
  368.                   logger.debug(_("Not adding desktopcouch pairing since"
  369.                   " the user has explicitly unpaired with Ubuntu One"))
  370.               else:
  371.                   logger.debug(_("Not adding desktopcouch pairing since"
  372.                   " we are already paired"))
  373.           if not found:
  374.               put_static_paired_service(None, "ubuntuone")
  375.               logger.debug(_("Pairing desktopcouch with Ubuntu One"))
  376.       
  377.       def main(self):
  378.             """Starts the gtk main loop."""
  379.             from twisted.internet import reactor
  380.             if self.connect != 2 or (self.connect == 1 and self.connected):
  381.                   gobject.idle_add(self.check_for_token, True)
  382.  
  383.             reactor.run()
  384.  
  385.       @property
  386.       def authorized(self):
  387.             """Are we authorized?"""
  388.             return self.is_authorized
  389.  
  390.       def sd_dbus_error(self, error):
  391.             """Got an error from DBus."""
  392.             self.__icon.sd_dbus_error(error)
  393.  
  394.  
  395.  
  396. def do_config_open():
  397.       """Opens the preferences dialog."""
  398.       paths = os.environ["PATH"].split(":")
  399.       dirpath = os.path.join(os.getcwd(), "bin")
  400.       if os.path.isdir(dirpath):
  401.             paths.insert(0, dirpath)
  402.       os.environ["PATH"] = ":".join(paths)
  403.       try:
  404.             ret = subprocess.call(["ubuntuone-client-preferences"])
  405.       except OSError:
  406.             ret = -1
  407.       if ret != 0:
  408.             logger.error(_("Failed to open Ubuntu One preferences"))
  409.  
  410. def do_xdg_open(path_or_url):
  411.       """Utility method to run xdg-open with path_or_url."""
  412.       ret = subprocess.call(["xdg-open", path_or_url])
  413.       if ret != 0:
  414.             logger.error(_("Failed to run 'xdg-open %s'") % path_or_url)
  415.  
  416. class AppletIcon(gtk.StatusIcon):
  417.       """
  418.       Custom StatusIcon derived from gtk.StatusIcon which supports
  419.       animated icons and a few other nice things.
  420.       """
  421.  
  422.       def __init__(self, main=None, config=None, *args, **kw):
  423.             """Initializes our custom StatusIcon based widget."""
  424.             super(AppletIcon, self).__init__(*args, **kw)
  425.             # Hide until we get status, to avoid flickering
  426.             self.set_visible(False)
  427.  
  428.             # The AppletMain object
  429.             self.__main = main
  430.  
  431.             # A ConfigParser object that we can poke at
  432.             self.__config = config
  433.             self.__show_when = self.__config.getint("ubuntuone", "show_applet")
  434.  
  435.             self.__managed_dir = None
  436.  
  437.             self.__size = 24
  438.             self.__theme = gtk.icon_theme_get_default()
  439.             iconpath = os.path.abspath(os.path.join(
  440.                         os.path.split(os.path.dirname(__file__))[0],
  441.                         "data"))
  442.             self.__theme.append_search_path(iconpath)
  443.  
  444.             self.__theme.append_search_path(os.path.sep + os.path.join(
  445.                         "usr", "share", "ubuntuone-client", "icons"))
  446.             self.__theme.append_search_path(os.path.sep + os.path.join(
  447.                         "usr", "local", "share", "ubuntuone-client", "icons"))
  448.  
  449.             self.set_from_icon_name('ubuntuone-client-offline')
  450.             self.set_tooltip(_("Disconnected"))
  451.             self.connect("popup-menu", self.__popup_menu)
  452.             self.connect("activate", self.__do_action)
  453.  
  454.             self.__size_changed(self, self.__size)
  455.  
  456.             self.__litems = {}
  457.             self.__ritems = {}
  458.             self.__status_menu, self.__config_menu = self.__build_menus()
  459.  
  460.             self.__connected = False
  461.             self.__need_update = False
  462.             self.__fatal_error = False
  463.  
  464.             pynotify.init("Ubuntu One")
  465.  
  466.             # Managing applet visibility
  467.             self.__visible = True
  468.             self.__visible_id = 0
  469.  
  470.             # Up/Dn status
  471.             self.__lock = Lock()
  472.             self.__updating = 0
  473.             self.__total = 0
  474.             self.__last_id = 0
  475.  
  476.             self.__bus = dbus.SessionBus()
  477.  
  478.             # Our own DBus service, for the config to deal with
  479.             self.__service = AppletConfig(icon=self)
  480.  
  481.             # DBus signal handling
  482.             self.__bus.add_signal_receiver(
  483.                   handler_function=self.__status_changed,
  484.                   signal_name="StatusChanged",
  485.                   dbus_interface=DBUS_IFACE_STATUS_NAME)
  486.  
  487.             self.__bus.add_signal_receiver(
  488.                   handler_function=self.__queue_changed,
  489.                   signal_name="ContentQueueChanged",
  490.                   dbus_interface=DBUS_IFACE_STATUS_NAME)
  491.             self.__bus.add_signal_receiver(
  492.                   handler_function=self.__transfer_started,
  493.                   signal_name="UploadStarted",
  494.                   dbus_interface=DBUS_IFACE_STATUS_NAME)
  495.             self.__bus.add_signal_receiver(
  496.                   handler_function=self.__transfer_started,
  497.                   signal_name="DownloadStarted",
  498.                   dbus_interface=DBUS_IFACE_STATUS_NAME)
  499.  
  500.             gobject.idle_add(self.__get_root)
  501.  
  502.       def set_from_icon_name(self, icon):
  503.             """Handle fallbacks for setting our icon."""
  504.             pixbuf = self.__theme.load_icon(icon, self.__size,
  505.                                             gtk.ICON_LOOKUP_GENERIC_FALLBACK)
  506.             self.set_from_pixbuf(pixbuf)
  507.  
  508.       def set_visibility_config(self, visibility):
  509.             """Update the visibility configuration."""
  510.             self.__show_when = int(visibility)
  511.             self.__config.set("ubuntuone", "show_applet", str(self.__show_when))
  512.             self.update_visibility()
  513.  
  514.       def set_connection_config(self, connect):
  515.             """Update the connection config."""
  516.             self.__config.set("ubuntuone", "connect", str(connect))
  517.  
  518.       def __update_transfer_status(self, done=False):
  519.             """Update the status display."""
  520.             with self.__lock:
  521.                   text = _("Updating %(transfers)d of %(total)d files...") % (
  522.                         { 'transfers' : self.__updating,
  523.                           'total' : self.__total})
  524.             label = self.__litems["status"].get_child()
  525.             if done:
  526.                   self.set_tooltip(_("Files updated."))
  527.                   self.set_from_icon_name("ubuntuone-client-idle")
  528.                   label.set_text(_("Your files are up to date."))
  529.             else:
  530.                   label.set_markup("<i>%s</i>" % text)
  531.  
  532.       def __queue_changed(self, queue):
  533.             """Handle ContentQueueChanged."""
  534.             total = 0
  535.             d = queue.get('Download', None)
  536.             if d is not None:
  537.                   total += int(d.get('count', 0))
  538.             d = queue.get('Upload', None)
  539.             if d is not None:
  540.                   total += int(d.get('count', 0))
  541.             first = False
  542.             last = False
  543.             self.__visible = True
  544.             self.set_tooltip(_("Updating files..."))
  545.             self.update_visibility()
  546.             with self.__lock:
  547.                   if self.__total == 0:
  548.                         first = True
  549.                         last = False
  550.                   if self.__total != 0 and total == 0:
  551.                         first = False
  552.                         last = True
  553.                   self.__total = total + self.__updating
  554.             if first:
  555.                   self.set_from_icon_name("ubuntuone-client-updating")
  556.                   n = pynotify.Notification(
  557.                         _("Updating files..."),
  558.                         _("Ubuntu One is now updating your files."))
  559.                   pixbuf = self.__theme.load_icon(
  560.                         "ubuntuone-client-updating", NOTIFY_ICON_SIZE,
  561.                         gtk.ICON_LOOKUP_GENERIC_FALLBACK)
  562.                   n.set_icon_from_pixbuf(pixbuf)
  563.                   n.show()
  564.             if last:
  565.                   if self.__last_id != 0:
  566.                         gobject.source_remove(self.__last_id)
  567.                         self.__last_id = 0
  568.                   self.__last_id = gobject.timeout_add_seconds(
  569.                         15, self.__updating_completed)
  570.  
  571.       def __updating_completed(self):
  572.             """Timeout to avoid multiple started/finished notifications."""
  573.             really_last = False
  574.             n = None
  575.             with self.__lock:
  576.                   done = self.__total - self.__updating
  577.                   if done == 0:
  578.                         really_last = True
  579.             if not really_last:
  580.                   return False
  581.  
  582.             with self.__lock:
  583.                   n = pynotify.Notification(
  584.                         _("Updating Finished"),
  585.                         P_("Ubuntu One finished updating %(total)d file.",
  586.                            "Ubuntu One finished updating %(total)d files.",
  587.                            self.__total) % { 'total' : self.__total })
  588.                   self.__total = 0
  589.                   self.__updating = 0
  590.             pixbuf = self.__theme.load_icon(
  591.                   "ubuntuone-client-updating", NOTIFY_ICON_SIZE,
  592.                   gtk.ICON_LOOKUP_GENERIC_FALLBACK)
  593.             n.set_icon_from_pixbuf(pixbuf)
  594.             n.show()
  595.             self.__update_transfer_status(True)
  596.             return False
  597.  
  598.       def __transfer_started(self, path):
  599.             """Handle the started signals."""
  600.             with self.__lock:
  601.                   self.__updating += 1
  602.             self.__update_transfer_status()
  603.  
  604.       def update_visibility(self):
  605.             """Update the icon's visibility."""
  606.             if self.__visible_id != 0:
  607.                   gobject.source_remove(self.__visible_id)
  608.                   self.__visible_id = 0
  609.  
  610.             if (self.__visible and self.__show_when != 2) or self.__fatal_error:
  611.                   self.set_visible(True)
  612.                   return
  613.  
  614.             if self.__show_when == 2 and not self.__fatal_error:
  615.                   self.set_visible(False)
  616.                   return
  617.  
  618.             # If the icon is shown, set up a timeout to hide it
  619.             if self.get_visible():
  620.                   self.__visible_id = gobject.timeout_add_seconds(
  621.                         30, self.__hide_icon)
  622.  
  623.       def __status_changed(self, status):
  624.             """The sync daemon status changed."""
  625.             if self.__managed_dir is None:
  626.                 gobject.idle_add(self.__get_root)
  627.  
  628.             if self.__show_when != 0:
  629.                   self.__visible = False
  630.  
  631.             state = status['name']
  632.  
  633.             self.set_tooltip("Ubuntu One")
  634.  
  635.             if self.__fatal_error and state != "UNKNOWN_ERROR":
  636.                   # Just blow your nose, and it's fixed, isn't it.
  637.                   self.__fatal_error = False
  638.                   self.__litems["connect"].set_sensitive(True)
  639.                   self.__litems["disconnect"].set_sensitive(True)
  640.                   
  641.             if state == "OFFLINE" or state.startswith("INIT") or \
  642.                       state.startswith("READY"):
  643.                   self.set_from_icon_name("ubuntuone-client-offline")
  644.                   self.set_tooltip(_("Disconnected"))
  645.                   self.__connected = False
  646.                   self.__visible = True
  647.  
  648.             elif state == "CAPABILITIES_MISMATCH":
  649.                   self.__connected = False
  650.                   self.__visible = True
  651.                   # Pop up a notification
  652.                   n = pynotify.Notification(
  653.                         _("Capabilities Mismatch"),
  654.                         _("There was a capabilities mismatch while attempting "
  655.                           "to connect to the Ubuntu One server. You may "
  656.                           "have installed a newer version of the client, for "
  657.                           "which the server does not yet provide support. "
  658.                           "A new version of the server should be accessible "
  659.                           "soon. Please be patient while we update."))
  660.                   pixbuf = self.__theme.load_icon(
  661.                         "ubuntuone-client-error", NOTIFY_ICON_SIZE,
  662.                         gtk.ICON_LOOKUP_GENERIC_FALLBACK)
  663.                   n.set_icon_from_pixbuf(pixbuf)
  664.                   n.set_urgency(pynotify.URGENCY_CRITICAL)
  665.                   n.show()
  666.                   # Set the tooltip and icon on the applet
  667.                   self.set_tooltip(_("Capabilities mismatch with server."))
  668.                   self.set_from_icon_name("ubuntuone-client-error")
  669.  
  670.             elif state == "IDLE" or state.startswith("READING") or \
  671.                       state.startswith("SCANNING"):
  672.                   self.__connected = True
  673.                   if self.__show_when != 0:
  674.                         self.__visible = False
  675.  
  676.             elif state == "AUTH_FAILED":
  677.                   self.__stop_syncdaemon()
  678.                   self.set_from_icon_name("ubuntuone-client-error")
  679.                   self.set_tooltip(_("Authentication failed"))
  680.                   self.__connected = False
  681.                   self.__visible = True
  682.  
  683.                   def reauthorize_error(e):
  684.                         """Simple dbus error handler."""
  685.                         logger.error(_("Error clearing token: %s") % str(e))
  686.  
  687.                   try:
  688.                         def token_cleared():
  689.                               """Do the next step."""
  690.                               if self.__main:
  691.                                     self.__main.check_for_token(True)
  692.  
  693.                         client = self.__bus.get_object(DBUS_IFACE_AUTH_NAME,
  694.                             "/", follow_name_owner_changes=True)
  695.                         iface = dbus.Interface(client, DBUS_IFACE_AUTH_NAME)
  696.                         iface.clear_token(OAUTH_REALM, OAUTH_CONSUMER,
  697.                                           reply_handler=token_cleared,
  698.                                           error_handler=reauthorize_error)
  699.                   except DBusException, e:
  700.                         reauthorize_error(e)
  701.  
  702.             elif state == "UNKNOWN_ERROR":
  703.                   # Disable some menu items
  704.                   self.__litems["connect"].set_sensitive(False)
  705.                   self.__litems["disconnect"].set_sensitive(False)
  706.                   # Change the behavior to file a bug
  707.                   if self.__fatal_error:
  708.                         return
  709.  
  710.                   self.__fatal_error = True
  711.                   self.__visible = True
  712.  
  713.                   # Pop up a notification
  714.                   n = pynotify.Notification(
  715.                         "Ubuntu One",
  716.                         _("There was a fatal error in Ubuntu One. " +
  717.                           "This may be a bug in the software. "
  718.                           "Please click on the Ubuntu One icon " +
  719.                           "in your panel to report a bug."))
  720.                   pixbuf = self.__theme.load_icon(
  721.                         "ubuntuone-client-error", NOTIFY_ICON_SIZE,
  722.                         gtk.ICON_LOOKUP_GENERIC_FALLBACK)
  723.                   n.set_icon_from_pixbuf(pixbuf)
  724.                   n.set_urgency(pynotify.URGENCY_CRITICAL)
  725.                   n.show()
  726.                   # Set the tooltip and icon on the applet
  727.                   self.set_tooltip(_("Fatal Error"))
  728.                   self.set_from_icon_name("ubuntuone-client-error")
  729.  
  730.             else:
  731.                   self.__connected = True
  732.                   self.set_from_icon_name("ubuntuone-client-idle")
  733.                   if state.startswith("CONNECTING") or \
  734.                             state.startswith("START_CONNECTING") or \
  735.                             state.startswith("AUTHENTICATING") or \
  736.                             state.startswith("CONNECTED") or \
  737.                             state.startswith("START_CONNECTED"):
  738.                         self.set_from_icon_name("ubuntuone-client-idle")
  739.                         self.set_tooltip(_("Connecting"))
  740.                         self.__visible = True
  741.  
  742.             self.update_visibility()
  743.  
  744.             if self.__connected:
  745.                   self.__litems["connect"].hide()
  746.                   self.__litems["disconnect"].show()
  747.             else:
  748.                   self.__litems["connect"].show()
  749.                   self.__litems["disconnect"].hide()
  750.             self.__config.set("ubuntuone", "connected", self.__connected)
  751.  
  752.       def __hide_icon(self):
  753.             """Timeout to hide tray icon after a period of inactivity."""
  754.             if self.__show_when == 0:
  755.                   return False
  756.  
  757.             self.__visible = False
  758.             self.__visible_id = 0
  759.             self.set_visible(False)
  760.             return False
  761.  
  762.       def __get_root(self):
  763.             """Method to get the rootdir from the sync daemon."""
  764.             # Get the managed root directory
  765.             try:
  766.                   client = self.__bus.get_object(DBUS_IFACE_NAME, "/",
  767.                                                  follow_name_owner_changes=True)
  768.             except DBusException:
  769.                   return False
  770.  
  771.             iface = dbus.Interface(client, DBUS_IFACE_SYNC_NAME)
  772.             def got_root(root):
  773.                   """We got the root dir."""
  774.                   self.__managed_dir = root
  775.                   if os.path.isdir(self.__managed_dir) and \
  776.                             os.access(self.__managed_dir,
  777.                                       os.F_OK | os.R_OK):
  778.                         self.__ritems["open"].set_sensitive(True)
  779.                         self.__add_to_places()
  780.                   else:
  781.                         self.__ritems["open"].set_sensitive(False)
  782.  
  783.             def got_err(error):
  784.                   """Handle error from the dbus callback."""
  785.                   self.sd_dbus_error(error)
  786.                   self.__managed_dir = None
  787.                   self.__ritems["open"].set_sensitive(False)
  788.  
  789.             iface.get_rootdir(reply_handler=got_root, error_handler=got_err)
  790.  
  791.             # Now get the current status
  792.             try:
  793.                   client = self.__bus.get_object(DBUS_IFACE_NAME, "/status",
  794.                                                  follow_name_owner_changes=True)
  795.                   iface = dbus.Interface(client, DBUS_IFACE_STATUS_NAME)
  796.                   iface.current_status(reply_handler=self.__status_changed,
  797.                                        error_handler=self.sd_dbus_error)
  798.             except DBusException, e:
  799.                   self.sd_dbus_error(e)
  800.                   return False
  801.  
  802.             return False
  803.  
  804.       def __build_menus(self):
  805.             """Create the pop-up menu items."""
  806.             # Create the left-click menu
  807.             lmenu = gtk.Menu()
  808.  
  809.             self.__litems["status"] = gtk.MenuItem(
  810.                   label=_("Your files are up to date."))
  811.             lmenu.append(self.__litems["status"])
  812.             self.__litems["status"].set_sensitive(False)
  813.             self.__litems["status"].show()
  814.  
  815.             sep = gtk.SeparatorMenuItem()
  816.             lmenu.append(sep)
  817.             sep.show()
  818.  
  819.             self.__litems["connect"] = gtk.ImageMenuItem(
  820.                   stock_id=gtk.STOCK_CONNECT)
  821.             lmenu.append(self.__litems["connect"])
  822.             self.__litems["connect"].connect("activate", self.__toggle_state)
  823.             self.__litems["connect"].show()
  824.  
  825.             self.__litems["disconnect"] = gtk.ImageMenuItem(
  826.                   stock_id=gtk.STOCK_DISCONNECT)
  827.             lmenu.append(self.__litems["disconnect"])
  828.             self.__litems["disconnect"].connect("activate", self.__toggle_state)
  829.  
  830.             lmenu.show()
  831.  
  832.             # Create the right-click menu
  833.             rmenu = gtk.Menu()
  834.  
  835.             self.__ritems["bug"] = gtk.MenuItem(label=_("_Report a Problem"))
  836.             rmenu.append(self.__ritems["bug"])
  837.             self.__ritems["bug"].connect("activate", self.__report_problem)
  838.             self.__ritems["bug"].show()
  839.  
  840.             self.__ritems["open"] = gtk.MenuItem(label=_("_Open Folder"))
  841.             rmenu.append(self.__ritems["open"])
  842.             self.__ritems["open"].connect("activate", self.__open_folder)
  843.             self.__ritems["open"].set_sensitive(False)
  844.             self.__ritems["open"].show()
  845.  
  846.             self.__ritems["web"] = gtk.MenuItem(label=_("_Go to Web"))
  847.             rmenu.append(self.__ritems["web"])
  848.             self.__ritems["web"].connect("activate", self.__open_website)
  849.             self.__ritems["web"].show()
  850.  
  851.             self.__ritems["config"] = gtk.ImageMenuItem(
  852.                   stock_id=gtk.STOCK_PREFERENCES)
  853.             rmenu.append(self.__ritems["config"])
  854.             self.__ritems["config"].connect("activate", self.__open_config)
  855.             self.__ritems["config"].show()
  856.  
  857.             sep = gtk.SeparatorMenuItem()
  858.             rmenu.append(sep)
  859.             sep.show()
  860.  
  861.             self.__ritems["quit"] = gtk.ImageMenuItem(stock_id=gtk.STOCK_QUIT)
  862.             rmenu.append(self.__ritems["quit"])
  863.             self.__ritems["quit"].connect("activate", self.__quit_applet)
  864.             self.__ritems["quit"].show()
  865.  
  866.             rmenu.show()
  867.  
  868.             return lmenu, rmenu
  869.  
  870.       def __size_changed(self, icon, size, data=None):
  871.             """Callback for when the size changes."""
  872.             if size < 24:
  873.                   self.__size = 16
  874.             elif size >= 24 and size < 32:
  875.                   self.__size = 24
  876.             elif size >= 32 and size < 48:
  877.                   self.__size = 32
  878.             elif size >= 48 and size < 64:
  879.                   self.__size = 48
  880.             else:
  881.                   self.__size = size
  882.  
  883.       def __popup_menu(self, icon, button, timestamp, data=None):
  884.             """Pops up the context menu for the tray icon."""
  885.             if button == 0:
  886.                   self.__status_menu.popup(None, None,
  887.                                            gtk.status_icon_position_menu,
  888.                                            button, timestamp, icon)
  889.             else:
  890.                   self.__config_menu.popup(None, None,
  891.                                            gtk.status_icon_position_menu,
  892.                                            button, timestamp, icon)
  893.  
  894.       def __stop_syncdaemon(self):
  895.             """Tell the syncdaemon to quit."""
  896.             def quit_error(e):
  897.                   """Just log and ignore."""
  898.                   logger.error(_("Quit Error: %s") % e.get_dbus_message())
  899.  
  900.             try:
  901.                   client = self.__bus.get_object(DBUS_IFACE_NAME, "/",
  902.                                                  follow_name_owner_changes=True)
  903.                   iface = dbus.Interface(client, DBUS_IFACE_SYNC_NAME)
  904.                   iface.quit(reply_handler=dbus_async,
  905.                              error_handler=quit_error)
  906.             except DBusException, e:
  907.                   quit_error(e)
  908.  
  909.       def __quit_applet(self, menuitem, data=None):
  910.             """Quit the daemon and closes the applet."""
  911.             self.__stop_syncdaemon()
  912.  
  913.             from twisted.internet import reactor
  914.             reactor.stop()
  915.  
  916.       def __toggle_state(self, menuitem, data=None):
  917.             """Connects or disconnects the storage sync process."""
  918.             try:
  919.                   client = self.__bus.get_object(DBUS_IFACE_NAME, "/",
  920.                                                  follow_name_owner_changes=True)
  921.                   iface = dbus.Interface(client, DBUS_IFACE_SYNC_NAME)
  922.                   if self.__connected:
  923.                         iface.disconnect(reply_handler=dbus_async,
  924.                                          error_handler=self.sd_dbus_error)
  925.                   else:
  926.                         if self.__main and self.__main.authorized is False:
  927.                               self.__main.check_for_token(do_login=True)
  928.                         iface.connect(reply_handler=dbus_async,
  929.                                       error_handler=self.sd_dbus_error)
  930.             except DBusException, e:
  931.                   self.sd_dbus_error(e)
  932.  
  933.             self.__config.set("ubuntuone", "connected", not self.__connected)
  934.             with open(CONF_FILE, "w+b") as f:
  935.                   self.__config.write(f)
  936.  
  937.       def __open_folder(self, data=None):
  938.             """Opens the storage folder in the file manager."""
  939.             if not self.__managed_dir or not os.path.isdir(self.__managed_dir):
  940.                   return
  941.  
  942.             folder = "file://%s" % quote(self.__managed_dir)
  943.             do_xdg_open(folder)
  944.  
  945.       def __do_action(self, data=None):
  946.             """Handles the most appropriate action when the icon is clicked."""
  947.             if self.__fatal_error:
  948.                   self.__report_problem()
  949.                   self.__quit_applet(None)
  950.                   return
  951.  
  952.             if self.__need_update:
  953.                   do_xdg_open("apt:ubuntuone-storage-protocol?refresh=yes")
  954.                   return
  955.  
  956.             # Popup the status menu
  957.             self.emit("popup-menu", 0, gtk.get_current_event_time())
  958.  
  959.       def __report_problem(self, data=None):
  960.             """Runs apport to report a problem against our code."""
  961.             args = ["ubuntu-bug", "ubuntuone-client"]
  962.             ret = subprocess.call(args)
  963.             if ret != 0:
  964.                   logger.error(_("Failed to run 'ubuntu-bug'"))
  965.  
  966.       def __open_website(self, data=None, url=None):
  967.             """Opens the one.ubuntu.com web site."""
  968.             if url:
  969.                   do_xdg_open(url)
  970.             else:
  971.                   do_xdg_open("https://one.ubuntu.com/")
  972.  
  973.  
  974.       def __open_config(self, data=None):
  975.             """Opens the preferences dialog."""
  976.             do_config_open()
  977.  
  978.       def __add_to_places(self):
  979.             """Add the managed directory to the .gtk-bookmarks file."""
  980.             # Only add once
  981.             if self.__config.getboolean("ubuntuone", "bookmarked"):
  982.                   return
  983.  
  984.             path = os.path.join(os.path.expanduser("~"), ".gtk-bookmarks")
  985.             with open(path, "a+") as f:
  986.                   bookmarks_entry = "file://%s %s\n" % (
  987.                         quote(self.__managed_dir), BOOKMARK_NAME)
  988.                   in_file = False
  989.                   for line in f:
  990.                         if line == bookmarks_entry:
  991.                               in_file = True
  992.                   if not in_file:
  993.                         f.write(bookmarks_entry)
  994.  
  995.             self.__config.set("ubuntuone", "bookmarked", "True")
  996.             with open(CONF_FILE, "w+b") as f:
  997.                   self.__config.write(f)
  998.  
  999.       def sd_dbus_error(self, error):
  1000.             """
  1001.             Handle DBus errors for crucial syncdaemon calls,
  1002.             and change the applet behavior slightly.
  1003.             """
  1004.             logger.error(_("DBus Error: %s") % error.get_dbus_message())
  1005.             if self.__fatal_error:
  1006.                   return
  1007.  
  1008.             self.__fatal_error = True
  1009.             self.__status_changed({'name' : 'UNKNOWN_ERROR'})
  1010.  
  1011.  
  1012. class AppletConfig(dbus.service.Object):
  1013.       """DBus Service object"""
  1014.  
  1015.       def __init__(self, icon, *args, **kwargs):
  1016.             """Initialize our magic."""
  1017.             self.icon = icon
  1018.             self.path = "/config"
  1019.             self.bus = dbus.SessionBus()
  1020.             bus_name = dbus.service.BusName(APPLET_BUS_NAME,
  1021.                                             bus=self.bus)
  1022.             dbus.service.Object.__init__(self, bus_name=bus_name,
  1023.                                          object_path=self.path)
  1024.  
  1025.       @dbus.service.method(APPLET_CONFIG_NAME,
  1026.                            in_signature='i', out_signature='')
  1027.       def set_visibility_config(self, visibility):
  1028.             self.icon.set_visibility_config(visibility)
  1029.  
  1030.       @dbus.service.method(APPLET_CONFIG_NAME,
  1031.                            in_signature='i', out_signature='')
  1032.       def set_connection_config(self, connect):
  1033.             self.icon.set_connection_config(connect)
  1034.  
  1035.  
  1036. if __name__ == "__main__":
  1037.       gettext.bindtextdomain(clientdefs.GETTEXT_PACKAGE, clientdefs.LOCALEDIR)
  1038.       gettext.textdomain(clientdefs.GETTEXT_PACKAGE)
  1039.  
  1040.       # Register DBus service for making sure we run only one instance
  1041.       bus = dbus.SessionBus()
  1042.       if bus.request_name(APPLET_BUS_NAME, dbus.bus.NAME_FLAG_DO_NOT_QUEUE) == dbus.bus.REQUEST_NAME_REPLY_EXISTS:
  1043.             print _("Ubuntu One client applet already running, quitting")
  1044.             do_config_open()
  1045.             sys.exit(0)
  1046.  
  1047.       gtk.rc_parse_string(RCSTYLE)
  1048.  
  1049.       icon = AppletMain()
  1050.       icon.main()
  1051.